package com.mtons.mblog.base.model.annotation;

import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 缓存锁切面
 * @author yunhao.qi
 */
@Component
@Aspect
public class CacheLockAspect {

    private Logger logger = LoggerFactory.getLogger(CacheLockAspect.class);

    private static ThreadLocal<Boolean> threadLocal = new ThreadLocal<>();

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 自定义锁住的值切点
     */
    @Pointcut("@annotation(com.mtons.mblog.base.model.annotation.CacheLock)")
    public void cacheLockValues(){}


    /**
     * 加锁
     * @param joinPoint
     */
    @Before(value = "cacheLockValues()")
    public void lockKeyValues(JoinPoint joinPoint) {

        //1.获取注解
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();//获得切面当中方法签名
        Method method = methodSignature.getMethod();//获得签名方法
        CacheLock cacheLock= method.getAnnotation(CacheLock.class);

        //2.获取注解上的参数
        String key = cacheLock.key();
        String values = cacheLock.values();
        String reminder = cacheLock.reminder();
        long timeout = cacheLock.timeout();
        TimeUnit timeUnit = cacheLock.timeUnit();

        //3.入参判断
        if(key == null || "".equals(key) || values == null || "".equals(values)) {
            logger.error("[缓存锁] -- 传入key或value为空");
            return;
        }

        //4.生成费用中心的key，切分values
        String uniqueKey = cacheLock.prefix() + key;
        Set<String> valueSet = new HashSet(Arrays.asList(parseSpel(values, method, joinPoint.getArgs()).split(",")));
        Set<String> useRetainValueSet = new HashSet<>(valueSet);

        //设置缓存锁
        setCacheLock(uniqueKey, valueSet, useRetainValueSet, reminder, timeout, timeUnit);

        threadLocal.set(true);
    }

    /**
     * 设置缓存锁
     * @param uniqueKey
     * @param valueSet
     * @param useRetainValueSet
     * @param reminder
     * @param timeout
     * @param timeUnit
     */
    private void setCacheLock(String uniqueKey, Set<String> valueSet, Set<String> useRetainValueSet, String reminder, long timeout, TimeUnit timeUnit) {

        //从缓存中命中
        ValueOperations valueOperations = redisTemplate.opsForValue();
        if(redisTemplate.hasKey(uniqueKey)) {
            //缓存中命中
            Set<String> cacheValueSet = (Set<String>) valueOperations.get(uniqueKey);
            logger.info("[缓存锁] -- redis中存在持有锁的redis-values，redis-values为：" + JSONObject.toJSONString(cacheValueSet));
            //取交集
            useRetainValueSet.retainAll(cacheValueSet);
            if(useRetainValueSet.size() > 0) {
                //有交集
                logger.warn("[缓存锁] -- redis中持有锁的redis-values与传入values存在交集，交集为：" + JSONObject.toJSONString(useRetainValueSet));
                threadLocal.set(false);
                throw new RuntimeException(reminder + JSONObject.toJSONString(useRetainValueSet));
            } else {
                //无交集
                cacheValueSet.addAll(valueSet);
                if(timeout > 0) {
                    valueOperations.set(uniqueKey, cacheValueSet, timeout, timeUnit);
                } else {
                    valueOperations.set(uniqueKey, cacheValueSet);
                }
                logger.info("[缓存锁] -- redis中持有锁的redis-values与传入values不存在交集，做合并操作，合并后持有锁的redis-values" + JSONObject.toJSONString(cacheValueSet));
            }
        } else {
            //缓存中不存在
            if(timeout > 0) {
                valueOperations.set(uniqueKey, valueSet, timeout, timeUnit);
            } else {
                valueOperations.set(uniqueKey, valueSet);
            }
            logger.info("[缓存锁] -- reids中未存在持有的锁，做添加操作，添加后持有锁的redis-value" + JSONObject.toJSONString(valueSet));
        }
    }


    /**
     * 解锁
     * @param joinPoint
     */
    @After(value = "cacheLockValues()")
    public void unLockKeyValues(JoinPoint joinPoint) {

        if(threadLocal.get() != null && !threadLocal.get()) {
            logger.error("[缓存锁] -- 前置增强方法中检验锁已存在，后置方法不允许解锁");
            return;
        }

        //1.获取注解
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();//获得切面当中方法签名
        Method method = methodSignature.getMethod();//获得签名方法
        CacheLock cacheLock = method.getAnnotation(CacheLock.class);

        //2.获取注解上的参数
        String key = cacheLock.key();
        String values = cacheLock.values();
        long timeout = cacheLock.timeout();
        TimeUnit timeUnit = cacheLock.timeUnit();

        //3.入参判断
        if(key == null || "".equals(key) || values == null || "".equals(values)) {
            logger.error("[缓存锁] -- 传入key或value为空");
            return;
        }

        //4.生成费用中心的key，切分values
        String uniqueKey = cacheLock.prefix() + key;
        Set<String> valueSet = new HashSet(Arrays.asList(parseSpel(values, method, joinPoint.getArgs()).split(",")));

        //5.删除锁
        deleteCacheLock(uniqueKey, valueSet, timeout, timeUnit);
    }

    /**
     * 从缓存中删除指定的锁
     * @param uniqueKey
     * @param valueSet
     * @param timeout
     * @param timeUnit
     */
    private void deleteCacheLock(String uniqueKey, Set<String> valueSet, long timeout, TimeUnit timeUnit) {

        //从缓存中命中
        ValueOperations valueOperations = redisTemplate.opsForValue();
        if(redisTemplate.hasKey(uniqueKey)) {
            //缓存中命中
            Set<String> cacheValues = (Set<String>) valueOperations.get(uniqueKey);
            logger.info("[缓存锁] -- redis中持有锁的redis-values为：" + JSONObject.toJSONString(cacheValues));
            //解锁
            cacheValues.removeAll(valueSet);
            logger.info("[缓存锁] -- redis中持有锁，解锁后的redis-values为：" + JSONObject.toJSONString(cacheValues));
            if(timeout > 0) {
                valueOperations.set(uniqueKey, cacheValues, timeout, timeUnit);
            } else {
                valueOperations.set(uniqueKey, cacheValues);
            }
        } else {
            //缓存中未命中
            logger.warn("[缓存锁] -- 根据当前的key：{}，未获取到缓存中的锁" + uniqueKey);
        }
    }

    /**
     * 解析spel表达式
     * @param argStr
     * @param method
     * @param args
     * @return
     */
    private String parseSpel(String argStr, Method method, Object [] args){

        if(argStr == null || "".equals(argStr)) {
            logger.error("arg is null or length is 0");
            throw new RuntimeException("arg is null or length is 0");
        }

        //获取被拦截方法参数名列表(使用Spring支持类库)
        LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
        String [] paraNameArr=u.getParameterNames(method);

        //使用SPEL进行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        //把方法参数放入SPEL上下文中
        for(int i=0; i<paraNameArr.length; i++){
            context.setVariable(paraNameArr[i], args[i]);
        }

        return parser.parseExpression(argStr).getValue(context,String.class);
    }


}
